Pthread 并发编程(二)——自底向上深入理解线程

本文正在参与「金石方案 . 分割6万现金大奖」

前语

在本篇文章傍边首要给咱们介绍线程最根本的组成元素,以及在 pthread 傍边给咱们供给的一些线程的根本机制,由于许多言语的线程机制便是建立在 pthread 线程之上的,比方说 Python 和 Java,深化了解 pthread 的线程实现机制,能够极大的提升咱们关于言语线程的知道。希望能够协助咱们深化了解线程。

线程的根本元素

首先咱们需求了解一些咱们在运用线程的时分的常用的根本操作,假如不是很了解没有联系咱们在后续的文章傍边会细心谈论这些问题。

  • 线程的常见的根本操作:

    • 线程的创立。
    • 线程的停止。
    • 线程之间的同步。
    • 线程的调度。
    • 线程傍边的数据管理。
    • 线程与进程之间的交互。
  • 在 linux 傍边一切的线程和进程同享一个地址空间。

  • 进程与线程之间同享一些内核数据结构:

    • 打开的文件描述符。
    • 当前工作目录。
    • 用户 id 和用户组 id 。
    • 大局数据段的数据。
    • 进程的代码。
    • 信号(signals)和信号处理函数(signal handlers)。
  • 线程独有的:

    • 线程的 ID 。
    • 寄存器线程和栈空间。
    • 线程的栈傍边的局部变量和回来地址。
    • 信号掩码。
    • 线程自己的优先级。
    • errno。

在一切的 pthread 的接口傍边,只要当函数的回来值是 0 的时分表明调用成功。

线程等候

在 pthread 的实现傍边,每个线程都两个特性:joinable 和 detached,当咱们发动一个线程的时分 (pthread_create) 线程的默认特点是 joinable,所谓 joinable 是表明线程是能够运用 pthread_join 进行同步的。

当一个线程调用 pthread_join(T, ret),当这个函数回来的时分就表明线程 T 已经停止了,履行完结。那么就能够开释与线程 T 的相关的体系资源。

假如一个线程的状况是 detached 状况的话,当线程完毕的时分与这个线程相关的资源会被自动开释掉,将资源归还给体系,也就不需求其他的线程调用 pthread_join 来开释线程的资源。

pthread_join 函数签名如下:

int pthread_join(pthread_t thread, void **retval);
  • thread 表明等候的线程。
  • retval 假如 retval 不等于 NULL 则在 pthread_join 函数内部会将线程 thead 的退出状况拷贝到 retval 指向的地址。假如线程被撤销了,那么 PTHREAD_CANCELED 将会被放在 retval 指向的地址。
  • 函数的回来值
    • EDEADLK 表明检测到死锁了,比入两个线程都调用 pthread_join 函数等候对方履行完结。
    • EINVAL 线程不是一个 joinable 的线程,一种常见的状况便是 pthread_join 一个 detached 线程。
    • EINVAL 当调用 pthrea_join 等候的线程正在被别的线程调用 pthread_join 等候。
    • ESRCH 假如参数 thread 是一个无效的线程,比方没有运用 pthread_create 进行创立。
    • 0 表明函数调用成功。

鄙人面的程序傍边咱们运用 pthread_join 函数去等候一个 detached 线程:

#include <stdio.h>
#include <error.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
pthread_t t1, t2;
void* thread_1(void* arg) {
  int ret = pthread_detach(pthread_self());
  sleep(2);
  if(ret != 0)
    perror("");
  return NULL;
}
int main() {
  pthread_create(&t1, NULL, thread_1, NULL);
  sleep(1);
  int ret = pthread_join(t1, NULL);
  if(ret == ESRCH)
    printf("No thread with the ID thread could be found.\n");
  else if(ret == EINVAL) {
    printf("thread is not a joinable thread or Another thread is already waiting to join with this thread\n");
  }
  return 0;
}

上面的程序的输出成果如下所示:

$ ./join.out
thread is not a joinable thread or Another thread is already waiting to join with this thread

在上面的程序傍边咱们在一个 detached 状况的线程上运用 pthread_join 函数,因而函数的回来值是 EINVAL 表明线程不是一个 joinable 的线程。

在上面的程序傍边 pthread_self() 回来当前正在履行的线程,回来的数据类型是 pthread_t ,函数 pthread_detach(thread) 的首要作用是将传入的线程 thread 的状况变成 detached 状况。

咱们再来看一个错误的比方,咱们在一个无效的线程上调用 pthread_join 函数


#include <stdio.h>
#include <error.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
pthread_t t1, t2;
void* thread_1(void* arg) {
  int ret = pthread_detach(pthread_self());
  sleep(2);
  if(ret != 0)
    perror("");
  return NULL;
}
int main() {
  pthread_create(&t1, NULL, thread_1, NULL);
  sleep(1);
  int ret = pthread_join(t2, NULL);
  if(ret == ESRCH)
    printf("No thread with the ID thread could be found.\n");
  else if(ret == EINVAL) {
    printf("thread is not a joinable thread or Another thread is already waiting to join with this thread\n");
  }
  return 0;
}

上面的程序的输出成果如下:

$./oin01.out
No thread with the ID thread could be found.

在上面的程序傍边咱们并没有运用 t2 创立一个线程但是在主线程履行的代码傍边,咱们运用 pthread_join 去等候他,因而函数的回来值是一个 EINVAL 。

咱们再来看一个运用 retval 比方:

#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
void* func(void* arg)
{
  pthread_exit((void*)100);
  return NULL;
}
int main() {
  pthread_t t;
  pthread_create(&t, NULL, func, NULL);
  void* ret;
  pthread_join(t, &ret);
  printf("ret = %ld\n", (u_int64_t)(ret));
  return 0;
}

上面的程序的输出成果如下所示:

$./understandthread/join03.out
ret = 100

在上面的程序傍边咱们运用一个参数 ret 去获取线程的退出码,从上面的成果咱们能够知道,咱们得到了正确的成果。

假如咱们没有在线程履行的函数傍边运用 pthread_exit 函数傍边清晰的指出线程的退出码,线程的退出码便是函数的回来值。比方下面的的程序:

#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
void* func(void* arg)
{
  return (void*)100;
}
int main() {
  pthread_t t;
  pthread_create(&t, NULL, func, NULL);
  void* ret;
  pthread_join(t, &ret);
  printf("ret = %ld\n", (u_int64_t)(ret));
  return 0;
}

上面的程序的输出成果也是 100 ,这与咱们期待的成果是共同的。

获取线程的栈帧和PC值

在多线程的程序傍边,每个线程具有自己的栈帧和PC寄存器(履行的代码的位置,在 x86_86 里边便是 rip 寄存器的值)。鄙人面的程序傍边咱们能够得到程序在履行时分的三个寄存器 rsp, rbp, rip 的值,咱们能够看到,两个线程履行时分的输出是不共同的,这个也从旁边面反映出来线程是具有自己的栈帧和PC值的。

#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
u_int64_t rsp;
u_int64_t rbp;
u_int64_t rip;
void find_rip() {
  asm volatile(
    "movq 8(%%rbp), %0;"
    :"=r"(rip)::
  );
}
void* func(void* arg) {
  printf("In func\n");
  asm volatile(             \
    "movq %%rsp, %0;"       \
    "movq %%rbp, %1;"       \
    :"=m"(rsp), "=m"(rbp):: \
  );
  find_rip();
  printf("stack frame: rsp = %p rbp = %p rip = %p\n", (void*)rsp, (void*)rbp, (void*) rip);
  return NULL;
}
int main() {
  printf("================\n");
  printf("In main\n");
  asm volatile(             \
    "movq %%rsp, %0;"       \
    "movq %%rbp, %1;"       \
    :"=m"(rsp), "=m"(rbp):: \
  );
  find_rip();
  printf("stack frame: rsp = %p rbp = %p rip = %p\n", (void*)rsp, (void*)rbp, (void*) rip);
  printf("================\n");
  pthread_t t;
  pthread_create(&t, NULL, func, NULL);
  pthread_join(t, NULL);
  return 0;
}

上面的程序的输出成果如下所示:

================
In main
stack frame: rsp = 0x7ffc47096d50 rbp = 0x7ffc47096d80 rip = 0x4006c6
================
In func
stack frame: rsp = 0x7f0a60d43ee0 rbp = 0x7f0a60d43ef0 rip = 0x400634

从上面的成果来看主线程和线程 t 履行的是不同的函数,而且两个函数的栈帧距离还是很大的,咱们核算一下 0x7ffc47096d80 – 0x7f0a60d43ef0 = 1038949363344 = 968G 的内存,因而很明显这两个线程运用的是不同的栈帧。

线程的线程号

在 pthread 傍边的一个线程对应一个内核的线程,内核和 pthread 都给线程保护了一个线程的 id 号,咱们能够运用 gettid 获取操作体系给咱们保护的线程号,运用函数 pthread_self 得到 pthread 线程库给咱们保护的线程号!

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
void* func(void* arg) {
  printf("pthread id = %ld tid = %d\n", pthread_self(), (int)gettid());
  return NULL;
}
int main() {
  pthread_t t;
  pthread_create(&t, NULL, func, NULL);
  pthread_join(t, NULL);
  return 0;
}

上面的程序的输出成果如下:

pthread id = 140063790135040 tid = 161643

线程与信号

在 pthread 库傍边首要给咱们供给了一些函数用于信号处理,咱们在 pthread 库傍边能够通过函数 pthread_kill 给其他的进程发送信号。

 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

咱们能够在一个线程傍边呼应其他线程发送过来的信号,而且呼应信号处理函数,在运用具体的比方深化了解线程的信号机制之前,首先咱们需求了解到的是在 pthread 多线程的程序傍边一切线程是同享信号处理函数的,假如在一个线程傍边修正了信号处理函数,这个成果是会影响其他线程的。

#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
void sig(int signo) {
  char s[1024];
  sprintf(s, "signo = %d tid = %d pthread tid = %ld\n", signo, gettid(), pthread_self());
  write(STDOUT_FILENO, s, strlen(s));
}
void* func(void* arg) {
  printf("pthread tid = %ld\n", pthread_self());
  for(;;);
  return NULL;
}
int main() {
  signal(SIGHUP, sig);
  signal(SIGTERM, sig);
  signal(SIGSEGV, sig);
  pthread_t t;
  pthread_create(&t, NULL, func, NULL);
  sleep(1);
  pthread_kill(t, SIGHUP);
  sleep(1);
  return 0;
}

上面的程序的输出成果如下所示:

pthread tid = 140571386894080
signo = 1 tid = 7785 pthread tid = 140571386894080

在上面的程序傍边,咱们首先在主函数里边从头界说了几个信号的处理函数,将 SIGHUP、SIGTERM 和 SIGSEGV 信号的处理函数悉数声明为函数 sig ,进程傍边的线程接受到这个信号的时分就会调用对应的处理函数,在上面的程序傍边主线程会给线程 t 发送一个 SIGHUP 信号,依据前面信号和数据对应联系咱们能够知道 SIGHUP 对应的信号的数字等于 1 ,咱们在信号处理函数傍边的确得到了这个信号。

除此之外咱们还能够设置线程自己的信号掩码,在前文傍边咱们已经提到了,每个线程都具有线程自己的掩码,因而鄙人面的程序傍边只要线程 2 呼应了主线程发送的 SIGTERM 信号。

#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
void sig(int signo) {
  char s[1024];
  sprintf(s, "signo = %d tid = %d pthread tid = %ld\n", signo, gettid(), pthread_self());
  write(STDOUT_FILENO, s, strlen(s));
}
void* func(void* arg) {
  sigset_t set;
  sigemptyset(&set);
  sigaddset(&set, SIGTERM);
  pthread_sigmask(SIG_BLOCK, &set, NULL);
  // 上面的代码的功用是堵塞 SIGTERM 这个信号 当这个信号传输过来的时分不会当即履行信号处理函数
  // 而是会比及将这个信号变成非堵塞的时分才会呼应
  printf("func : pthread tid = %ld\n", pthread_self());
  for(;;);
  return NULL;
}
void* func02(void* arg) {
  printf("func02 : pthread tid = %ld\n", pthread_self());
  for(;;);
  return NULL;
}
int main() {
  signal(SIGTERM, sig);
  pthread_t t1;
  pthread_create(&t1, NULL, func, NULL);
  sleep(1);
  pthread_t t2;
  pthread_create(&t2, NULL, func02, NULL);
  sleep(1);
  pthread_kill(t1, SIGTERM);
  pthread_kill(t2, SIGTERM);
  sleep(2);
  return 0;
}

在上面的程序傍边咱们创立了两个线程而且界说了 SIGTERM 的信号处理函数,在线程 1 履行的函数傍边修正了自己堵塞的信号集,将 SIGTERM 变成了一种堵塞信号,也便是说当线程接受到 SIGTERM 的信号的时分不会当即调用 SIGTERM 的信号处理函数,只要将这个信号变成非堵塞的时分才能够呼应这个信号,履行对应的信号处理函数,但是线程 t2 并没有堵塞信号 SIGTERM ,因而线程 t2 会履行对应的信号处理函数,上面的程序的输出成果如下所示:

func : pthread tid = 139887896323840
func02 : pthread tid = 139887887931136
signo = 15 tid = 10652 pthread tid = 139887887931136

依据上面程序的输出成果咱们能够知道线程 t2 的确调用了信号处理函数(依据 pthread tid )能够判别,而线程 t1 没有履行信号处理函数。

在上文傍边咱们还提到了在一个进程傍边,一切的线程同享同一套信号处理函数,假如在一个线程里边从头界说了一个信号的处理函数,那么他将会影响其他的线程,比方下面的程序:

#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
void sig(int signo) {
  char s[1024];
  sprintf(s, "signo = %d tid = %d pthread tid = %ld\n", signo, gettid(), pthread_self());
  write(STDOUT_FILENO, s, strlen(s));
}
void sig2(int signo) {
  char* s = "thread-defined\n";
  write(STDOUT_FILENO, s, strlen(s));
}
void* func(void* arg) {
  signal(SIGSEGV, sig2);
  printf("pthread tid = %ld\n", pthread_self());
  for(;;);
  return NULL;
}
void* func02(void* arg) {
  printf("pthread tid = %ld\n", pthread_self());
  for(;;);
  return NULL;
}
int main() {
  signal(SIGSEGV, sig);
  pthread_t t;
  pthread_create(&t, NULL, func, NULL);
  sleep(1);
  pthread_t t2;
  pthread_create(&t2, NULL, func02, NULL);
  sleep(1);
  pthread_kill(t2, SIGSEGV);
  sleep(2);
  return 0;
}

上面的程序的输出成果如下所示:

pthread tid = 140581246330624
pthread tid = 140581237937920
thread-defined

从上面程序输出的成果咱们能够看到线程 t2 履行的信号处理函数是 sig2 而这个信号处理函数是在线程 t1 履行的函数 func 傍边进行修正的,能够看到线程 t1 修正的成果的确得到了呼应,从这一点也能够看出,假如一个线程修正信号处理函数是会影响到其他的线程的。

总结

在本篇文章傍边首要介绍了一些根底了线程自己的特性,而且运用一些比方去验证了这些特性,协助咱们从根本上去了解线程,其实线程触及的东西实在太多了,在本篇文章里边仅仅列举其中的部分比方进行运用说明,在后续的文章傍边咱们会持续深化的去谈这些机制,比方线程的调度,线程的撤销,线程之间的同步等等。

更多精彩内容合集可访问项目:github.com/Chang-LeHun…

关注大众号:一无是处的研讨僧,了解更多核算机(Java、Python、核算机体系根底、算法与数据结构)常识。

本文正在参与「金石方案 . 分割6万现金大奖」